Extensions
Overview
DataObjects.Net Extensions are small projects that extend standard functionality of DataObjects.Net core. They are maintained by Xtensive engineers and volunteers from DataObjects.Net community. Each extension has a corresponding NuGet package so they can be installed separately or in any combination. Using NuGet is a recommended way of installing the extensions.
 
Starting from v6.0 the extensions source code is available on GitHub as part of main repository and binaries are published to NuGet package library.
Bulk Operations
The extension provides a set of IQueryable extension methods that are translated
to server-side UPDATE or DELETE commands.
How to use
Add Bulk Operations package to your project.
Demo
- Update primitive property with a constant value, e.g.: 
//sync
Query.All<Bar>()
  .Where(a => a.Id == 1)
  .Set(a => a.Count, 2)
  .Update();
//async
await Query.All<Bar>()
  .Where(a => a.Id == 1)
  .Set(a => a.Count, 2)
  .UpdateAsync();
- Updating persistent property with expression, computed on server, e.g.: 
//sync
Query.All<Bar>()
 .Where(a => a.Id==1)
  .Set(a => a.Count, a => a.Description.Length)
  .Update();
//async
await Query.All<Bar>()
  .Where(a => a.Id==1)
  .Set(a => a.Count, a => a.Description.Length)
  .UpdateAsync();
- Setting a reference to an entity that is already loaded into current - Session
// Emulating entity loading
var bar = Query.Single<Bar>(1);
//sync
Query.All<Foo>()
  .Where(a => a.Id == 2)
  .Set(a => a.Bar, bar)
  .Update();
// Emulating entity loading
var bar = Query.Single<Bar>(1);
//async
await Query.All<Foo>()
  .Where(a => a.Id == 2)
  .Set(a => a.Bar, bar)
  .UpdateAsync();
- Setting a reference to an entity that is not loaded into - Session, 1-st way
//sync
Query.All<Foo>()
  .Where(a => a.Id == 1)
  .Set(a => a.Bar, a => Query.Single<Bar>(1))
  .Update();
//async
await Query.All<Foo>()
  .Where(a => a.Id == 1)
  .Set(a => a.Bar, a => Query.Single<Bar>(1))
  .UpdateAsync();
- Setting a reference to an entity that is not loaded into - Session, 2-nd way
//sync
Query.All<Foo>()
  .Where(a => a.Id == 1)
  .Set(a => a.Bar, a => Query.All<Bar>().Single(b => b.Name == "test"))
  .Update();
//async
await Query.All<Foo>()
  .Where(a => a.Id == 1)
  .Set(a => a.Bar, a => Query.All<Bar>().Single(b => b.Name == "test"))
  .UpdateAsync();
- Constructing update expressions of the fly 
//sync
bool condition = CheckCondition();
var query = Query.All()<Bar>
  .Where(a => a.Id == 1)
  .Set(a => a.Count, 2);
if(condition)
  query = query.Set(a => a.Name, a => a.Name + "test");
query.Update();
//async
bool condition = CheckCondition();
var query = Query.All()<Bar>
  .Where(a => a.Id == 1)
  .Set(a => a.Count, 2);
if(condition)
  query = query.Set(a => a.Name, a => a.Name + "test");
await query.UpdateAsync();
- Updating lots of properties at once 
//sync
Query.All<Bar>()
  .Where(a => a.Id == 1)
  .Update(a => new Bar(null) { Count = 2, Name = a.Name + "test", dozens of other properties... });
//async
await  Query.All<Bar>()
  .Where(a => a.Id == 1)
  .UpdateAsync(a => new Bar(null) { Count = 2, Name = a.Name + "test", dozens of other properties... });
- Deleting entities 
//sync
Query.All<Foo>()
  .Where(a => a.Id == 1)
  .Delete();
//async
await Query.All<Foo>()
  .Where(a => a.Id == 1)
  .DeleteAsync();
Localization/Internationalization
The extension transparently solves a task of application or service localization. This implies that localizable resources are a part of domain model so they are stored in database.
How to use
- Add Localization package to your project. 
- Include types from - Xtensive.Orm.Localizationassembly into the domain:
<Xtensive.Orm>
  <domains>
    <domain name="Default" >
      <types>
        <add assembly="your assembly"/>
        <add assembly="Xtensive.Orm.Localization"/>
      </types>
    </domain>
  </domains>
</Xtensive.Orm>
- Implement - ILocalizable<TLocalization>on your localizable entities, e.g.:
[HierarchyRoot]
public class Page : Entity, ILocalizable<PageLocalization>
{
  [Field, Key]
  public int Id { get; private set; }
  // Localizable field. Note that it is non-persistent
  public string Title
  {
    get { return Localizations.Current.Title; }
    set { Localizations.Current.Title = value; }
  }
  [Field] // This is a storage of all localizations for Page class
  public LocalizationSet<PageLocalization> Localizations { get; private set; }
  public Page(Session session) : base(session) {}
}
- Define corresponding localizations, e.g.: 
[HierarchyRoot]
public class PageLocalization : Localization<Page>
{
  [Field(Length = 100)]
  public string Title { get; set; }
  public PageLocalization(Session session, CultureInfo culture, Page target)
    : base(session, culture, target) {}
}
Demo
- Access localizable properties as regular ones, e.g.: 
page.Title = "Welcome";
string title = page.Title;
- Mass editing of localizable properties: 
var en = new CultureInfo("en-US");
var sp = new CultureInfo("es-ES");
var page = new Page(session);
page.Localizations[en].Title = "Welcome";
page.Localizations[sp].Title = "Bienvenido";
- Value of localizable properties reflects culture of the current Thread, e.g.: 
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
string title = page.Title; // title is "Welcome"
Thread.CurrentThread.CurrentCulture = new CultureInfo("es-ES");
string title = page.Title; // title is "Bienvenido"
- Instead of altering CurrentThread, instance of LocalizationScope can be used, e.g.: 
using (new LocalizationScope(new CultureInfo("en-US"))) {
  string title = page.Title; // title is "Welcome"
}
using (new LocalizationScope(new CultureInfo("es-ES"))) {
  string title = page.Title; // title is "Bienvenido"
}
- LINQ queries that include localizable properties are transparently translated 
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
var query = from p in session.Query.All<Page>()
  where p.Title=="Welcome"
  select p;
Assert.AreEqual(1, query.Count());
Thread.CurrentThread.CurrentCulture = new CultureInfo("es-ES");
var query = from p in session.Query.All<Page>()
  where p.Title=="Bienvenido"
  select p;
Assert.AreEqual(1, query.Count());
Reprocessing
The extension provides API for reprocessible operations. The reprocessible operation should represent a separate block of logic, usually a delegate of a method and be transactional.
How to use
- Add Reprocessing package to your project. 
Demo
- Simple reprocessible operation looks like this: 
Domain.Execute(session =>
  {
    // Task logic
  });
Session is provided by reprocessing infrastructure. No session activation perfromed so use the instance you were gived by delegate.
2. Use WithSession to pass session from ouside, it this case infrastructure won’t open
any session and uses yours instead. Don’t use session activation to pass session to delegate.
using (var externalSession = Domain.OpenSession()) {
  Domain.WithSession(externalSession)
    .Execute(session =>
      {
        // Task logic
      });
}
- There are 3 strategies that can be used for task execution: 
- HandleReprocessibleExceptionstrategy
 The strategy catches all reprocessible expections (deadlock and transaction serialization exceptions) and makes another attempt to execute the task
- HandleUniqueConstraintViolationstrategy
 The same as previous one but also catches unique constraint violation exception
- NoReprocessstrategy
 No reprocessing is provided
To indicate that a particular strategy should be used, use the following syntax:
Domain.WithStrategy(new HandleReprocessExceptionStrategy())
  .Execute(session =>
    {
      // Task logic
    });
4. To omit setting up the strategy each time consider configuring it in application configuration file, e.g.:
<configSections>
  ...
  <section name="Xtensive.Orm.Reprocessing"
    type="Xtensive.Orm.Reprocessing.Configuration.ConfigurationSection, Xtensive.Orm.Reprocessing" />
</configSections>
<Xtensive.Orm.Reprocessing
  defaultTransactionOpenMode="New"
  defaultExecuteStrategy="Xtensive.Orm.Reprocessing.HandleReprocessableExceptionStrategy, Xtensive.Orm.Reprocessing">
</Xtensive.Orm.Reprocessing>
Having that done, in scenarios with no strategy specified, the extension will automatically use the strategy from the configuration.
Security
The extension provides security layer (authentication services, principals, roles, secured queries) There are 2 main parts that can also be used separately: authentication services and role-based access to domain entities
How to use
- Add Security package to your project. 
- Include types from - Xtensive.Orm.Securityassembly into the domain:
<Xtensive.Orm>
  <domains>
    <domain name="Default" >
      <types>
        <add assembly="your assembly"/>
        <add assembly="Xtensive.Orm.Security"/>
      </types>
    </domain>
  </domains>
</Xtensive.Orm>
- If you are planning to use one of authentication services add 
<section name="Xtensive.Orm.Security" type="Xtensive.Orm.Security.Configuration.ConfigurationSection,
Xtensive.Orm.Security" />
and set up the desired hashing service:
<Xtensive.Orm.Security>
  <hashingService name="plain"/>
  <!-- other options are: md5, sha1, sha256, sha384, sha512 -->
</Xtensive.Orm.Security>
Demo
- Define a class that inherits abstract - GenericPrincipalclass that will describe your users, e.g.:
[HierarchyRoot]
public class User : GenericPrincipal
{
  [Field, Key]
  public int Id { get; private set; }
  [Field]
  public string LastName { get; set; }
  [Field]
  public string FirstName { get; set; }
  //...
  public User(Session session) : base(session) {}
}
- Having the - Userclass defined, it can be used for user creation and authentication.
// Creating a user
using (var session = Domain.OpenSession()) {
  using (var transaction = session.OpenTransaction()) {
    var user = new User(session);
    user.Name = "admin";
    user.SetPassword("password");
    transaction.Complete();
  }
}
// Authenticating a user
using (var session = Domain.OpenSession()) {
  using (var transaction = session.OpenTransaction()) {
    var user = session.Authenticate("admin", "password");
    transaction.Complete();
  }
}
- Define a hierarchy of roles. A role is a set of permissions for a job function within a company, e.g.: 
EmployeeRole
|
|- StockManagerRole
|
|- SalesRepresentativeRole
   |
   |- SalesManagerRole
   |
   |- SalesPresidentRole
// This is base role for all employees
[HierarchyRoot(InheritanceSchema = InheritanceSchema.SingleTable)]
public abstract class EmployeeRole : Role
{
  [Field, Key]
  public int Id { get; set; }
  protected override void RegisterPermissions()
  {
    // All employees can read products
    RegisterPermission(new Permission<Product>());
    // All employees can read other employees
    RegisterPermission(new Permission<Employee>());
  }
  protected EmployeeRole(Session session)
    : base(session) {}
}
public class StockManagerRole : EmployeeRole
{
  protected override void RegisterPermissions()
  {
    // Stock manager inherits Employee permissions
    base.RegisterPermissions();
    // Stock manager can read and write products
    RegisterPermission(new Permission<Product>(canWrite:true));
  }
  public StockManagerRole(Session session)
    : base(session) {}
}
// Create instances of roles on first domain initialization
using (var session = Domain.OpenSession()) {
  using (var transaction = session.OpenTransaction()) {
    new SalesRepresentativeRole(session);
    new SalesManagerRole(session);
    new SalesPresidentRole(session);
    new StockManagerRole(session);
    transaction.Complete();
  }
}
- Members of staff are assigned particular roles, e.g.: 
using (var session = Domain.OpenSession()) {
  using (var transaction = session.OpenTransaction()) {
    var stockManagerRole = session.Query.All<StockManagerRole>().Single();
    var user = new User(session);
    user.Name = "peter";
    user.SetPassword("password");
    user.Roles.Add(stockManagerRole);
    transaction.Complete();
  }
}
- Checking whether a user has the required role 
user.IsInRole("StockManagerRole");
// or
user.Roles.Contains(stockManagerRole);
- Session impersonation 
using (var imContext = session.Impersonate(user)) {
  // inside the region the session is impersonated with the specified
  // principal and set of their roles and permissions
  // Checking whether the user has a permission for reading Customer entities
  imContext.Permissions.Contains<Permission<Customer>>(p => p.CanRead);
  // Checking whether the user has a permission for writing to Customer entities
  imContext.Permissions.Contains<Permission<Customer>>(p => p.CanWrite);
  // another way
  var p = imContext.Permissions.Get<Permission<Customer>>();
  if (p != null && p.CanRead)
    // allow doing some stuff
}
To end the impersonation call ImpersonationContext.Undo() or Dispose() method.
Impersonation contexts can be nested, e.g.:
using (var userContext = session.Impersonate(user)) {
  // do some user-related stuff
  using (var adminContext = session.Impersonate(admin)) {
    // do some admin stuff
  }
  // we are still in user impersonation context
}
// no context here
7. Secure (restrictive) queries 
A role may set up a condition that will be automatically added to any query and filters the query results, e.g.:
public class AutomobileManagerRole : EmployeeRole
{
  private static IQueryable<Customer> GetCustomers(ImpersonationContext context, QueryEndpoint query)
  {
    return query.All<Customer>()
      .Where(customer => customer.IsAutomobileIndustry);
  }
  protected override void RegisterPermissions()
  {
    base.RegisterPermissions();
    // This permission tells that a principal can read/write customers
    // but only those that are returned by the specified condition
    RegisterPermission(new CustomerPermission(true, GetCustomers));
  }
  public AutomobileManagerRole(Session session)
    : base(session) {}
}
Now all employees that have AutomobileManagerRole will read
customers that have IsAutomobileIndustry property set to true, e.g.:
using (var session = Domain.OpenSession()) {
  using (var transaction = session.OpenTransaction()) {
    var automobileManagerRole = session.Query.All<AutomobileManagerRole>().Single();
    var user = new User(session);
    user.Name = "peter";
    user.SetPassword("password");
    user.Roles.Add(automobileManagerRole);
    using (var context = session.Impersonate(user)) {
      var customers = Query.All<Customer>();
      // Inside the impersonation context the above-mentioned query condition
      // will be added automatically so user will get only automobile customers
    }
    transaction.Complete();
  }
}
Change Tracking/Auditing
The extension provides tracking/auditing funtionality on Session/Domain level.
How to use
- Add Tracking package to your project. 
- Include types from - Xtensive.Orm.Trackingassembly into the domain:
<Xtensive.Orm>
  <domains>
    <domain name="Default" >
      <types>
        <add assembly="your assembly"/>
        <add assembly="Xtensive.Orm.Tracking"/>
      </types>
    </domain>
  </domains>
</Xtensive.Orm>
- To track changes on - Sessionlevel obtain an instance of- ISessionTrackingMonitorthrough- Session.Services.Get<ISessionTrackingMonitor>()method. To track changes on- Domainlevel (from all sessions) obtain an instance of- IDomainTrackingMonitorthrough- Domain.Services.Get<IDomainTrackingMonitor>()method.
4. Subscribe to TrackingCompleted event. After each tracked transaction is committed you
receive the TrackingCompletedEventArgs object.
5. TrackingCompletedEventArgs.Changes contains a collection of ITrackingItem objects,
each of them represents a set of changes that occurred to an Entity within the transaction committed.
Demo
- Subscribe to - ISessionTrackingMonitor/- IDomainTrackingMonitor- TrackingCompletedevent
var monitor = Domain.Services.Get<IDomainTrackingMonitor>();
monitor.TrackingCompleted += TrackingCompletedListener;
- Do some changes to persistent entities 
using (var session = Domain.OpenSession()) {
 using (var t = session.OpenTransaction()) {
   var e = new MyEntity(session);
   e.Text = "some text";
   t.Complete();
 }
}
- Handle - TrackingCompletedevent call and do whatever you want with tracked changes.
private void TrackingCompletedListener(object sender, TrackingCompletedEventArgs e)
{
 foreach (var change in e.Changes) {
   Console.WriteLine(change.Key);
   Console.WriteLine(change.State);
   foreach (var value in change.ChangedValues) {
     Console.WriteLine(value.Field.Name);
     Console.WriteLine(value.OriginalValue);
     Console.WriteLine(value.NewValue);
   }
 }
}
Web
The extension adds integration for DataObjects.Net and ASP.NET Core. NOTE, that SessionManager class is deprecated.
We strongly recommend not to use SessionManager because it, it does not keep up with current trends.
New types use more natural mechanisms of ASP.NET Core such as Pipeline, dependancy injection (DI) and Action filters. It instead of static access
to session by using Session.Current, it is designed to provide a session instance directly.
The extension contains two ways of integration - by using Action Filters or by using Middleware. Both are available for ASP.Net MVC applications, Rasor Pages have to use only middleware.
Action Filter
The extension contains SessionActionFilter class. This filter opens Session and TransacionScope
before an action executed, puts them to request’s HttpContext and provides you a SessionAccessor
instance which is capable of getting the objects from the context. When action is completed, the action filter
removes Session and TransactionScope instances from the context and disposes them.
SessionActionFilter can be registered as action filter in Startup class like so:
public void ConfigureServices(IServiceCollection services)
{
  var domain = Domain.Build();
  services.AddSingleton<Domain>(domain);// make domain accessible as service in action filter
  services.AddDataObjectsSessionAccessor();// adds SessionAccessor as scoped service
  services.AddControllersWithViews(options => options.Filters.AddDataObjectsSessionActionFilter());
}
After that you use it like so:
public class HomeController : Controller
{
  private readonly ILogger<HomeController> _logger;
  // This action require Session and TransactionScope because it
  // has SessionAccessor as parameter. So the action filter will open them.
  public IActionResult Index([FromServices] SessionAccessor sessionAccessor)
  {
    // DO NOT dispose them within action because the action filter
    // controlls them.
    var session = sessionAccessor.Session;
    var transactionScope = sessionAccessor.TransactionScope;
    // some query to database
    var someQuery = session.Query.All<SomeEntity>().ToList()
    ...
    return View();
  }
  // This action does not require Session and TransactionScope
  // to be opened. In this case filter won't do any work - neither Session
  // nor TransactionScope will be opened.
  public IActionResult Privacy()
  {
    return View();
  }
  public HomeController(ILogger<HomeController> logger)
  {
    _logger = logger;
  }
}
Notice [FromServices] attribute for parameter, it is required.
The filter has an ability to skip openings if action does not require them. So the action filter gives you access to database when only you declare you need it.
By default, the filter completes the opened transaction scope unless an exception has appeared. It was done so to
mimic the work of obsolete SessionManager which also completes transaction scope by default. If you want to change
default behavior you can create your own filter based on ours and override CompleteTransactionOnException method
and define what exceptions will not rollback transaction.
All the methods you can override:
- OpenSession/- OpenSessionAsyncmethod - opens session.
- OpenTransaction/- OpenTransactionAsyncmethod - opens transaction scope.
- OnTransactionScopeDisposingmethod is called before transaction scope disposing. Default implementation completes transaction scope if no exception appeared or- CompleteTransactionOnExceptionreturned- true.
- CompleteTransactionOnExceptionmethod allows you to choose what exceptions won’t lead to transaction rollback. Additionally to exception thrown by action it gives you access to- ActionExecutedContext.
- OnTransactionScopeDisposedmethod is called when transaction scope has been disposed (transaction is either completed or rolled back).
- OnSessionDisposingmethod is called before session disposing.
- OnSessionDisposedmethod is called when session has been disposed.
The methods are called in the same order.
Middleware
The extension also has OpenSessionMiddleware class. The key feature of action filter is also its
drawback - it wraps only MVC action. If wider coverage needed then it’s better to use middleware because
it can wrap not only MVC action calls but also controller’s constructor calls and other middleware which
are defined down the pipeline.
To confure your app to use OpenSessionMiddleware, first, configure services in Startup class like so:
public void ConfigureServices(IServiceCollection services)
{
  var domain = Domain.Build();
  services.AddSingleton<Domain>(domain); // to have domain be accessible
  //infrastructure will create SessionAccessor instance
  //for each request.
  services.AddDataObjectsSessionAccessor();
  services.AddControllersWithViews();
}
Then, put OpenSessionMiddleware to desired place in the pipeline like so:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  app.UseStaticFiles()
    .UseAuthorization()
    .UseDataObjectsSessionOpener()
    // ...
    // any other middlewhare,
    // they will have access to
    // opened session
    // ...
    .UseRouting()
    .UseEndpoints(endpoints => {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
      });
  }
After that you can specify SessionAccessor as a parameter in MVC controllers’ constructors,
actions. The SessionOpener will open Session and TransactionScope on going forward
and dispose them on going backwards the pipeline.
public class HomeController : Controller
{
  private readonly ILogger<HomeController> _logger;
  public IActionResult Index([FromServices] SessionAccessor sessionAccessor)
  {
    var session = sessionAccessor.Session;
    var transactionScope = sessionAccessor.TransactionScope;
    // some query to database
    var someQuery = session.Query.All<SomeEntity>().ToList()
    ...
    return View();
  }
  public IActionResult Privacy()
  {
    return View();
  }
  public HomeController(ILogger<HomeController> logger, SessionAccessor accessor)
  {
    _logger = logger;
  }
}
Other middleware can get access to session like so:
public class SomeCustomMiddleware
{
  private readonly RequestDelegate next;
  // Dependancy Injection mechansims will provide accessor
  public async Task Invoke(HttpContext httpContext, SessionAccessor sessionAccessor)
  {
    // perform some actions before next middleware
    await next.Invoke(httpContext);
    // some actions after execution returned
  }
  public SomeCustomMiddleware(RequestDelegate next)
  {
    this.next = next
  }
}
As the action filter above it completes transaction scope by default unless an exception happens. It also re-throw the exception by default.
OpenSessionMiddleware is customizable as well. It has similar CompleteTransactionOnException
method to control what exceptions will not rollback transaction. Additionally, RethrowException method
allows to define what exceptions should be “swallowed”.
Besides these two methods the class contains following virtual methods:
- OpenSessionAsyncmethod opens- Session;
- OpenTransactionAsyncmethod opens- TransactionScope;
- OnTransactionDisposingAsyncmethod is called before- TransactionScopebecomes disposed;
- OnTransactionDisposedAsyncmethod is called after- TransactionScopebecomes disposed;
- OnSessionDisposingAsyncmethod is called before- Sessionbecomes disposed;
- OnSessionDisposedAsyncmethod is called after- Sessionbecomes disposed.
OpenSessionMiddleware and SessionActionFilter will work together just fine. If the action filter
detects that Session and TransactionScope have already been opended by the middleware it will
not open another one and let the middleware be in charge.